/*
 * Copyright 2008 Zoundry LLC, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package org.zoundry.gaedt.core.models;

import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import javax.xml.namespace.QName;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * A generic Sax content handler that is used when parsing a model file.  This
 * handler reads in the XML and produces a {@link ZGaeModel}.
 */
public class ZGaeSaxModelHandler extends DefaultHandler
{
    /** The model factory used to create the models. */
    private IZGaeModelFactory mModelFactory;
    /** Used to convert parsed attribute values into their correct type for setting on the model. */
    private IZGaeModelTypeMapper mTypeMapper;
    /** Used to find the mutator method to use when setting an attribute value on a model. */
    private IZGaeModelMutatorProvider mMutatorProvider;

    /** The root model. */
    private ZGaeModel mRootModel;
    /** The stack of models. */
    private Stack<ZGaeModel> mModelStack;
    /** The current namespace context. */
    private Map<String, String> mNamespaceContext;
    /** The locator. */
    private Locator mLocator;
    /** Contains character data, in case multiple callbacks are made for char data. */
    private StringBuffer mBuffer;

    /**
     * C'tor.
     * 
     * @param aModelFactory
     * @param aTypeMapper
     * @param aMutatorProvider
     */
    public ZGaeSaxModelHandler(IZGaeModelFactory aModelFactory, IZGaeModelTypeMapper aTypeMapper,
            IZGaeModelMutatorProvider aMutatorProvider)
    {
        setModelFactory(aModelFactory);
        setTypeMapper(aTypeMapper);
        setMutatorProvider(aMutatorProvider);

        setModelStack(new Stack<ZGaeModel>());
        setNamespaceContext(new HashMap<String, String>());
    }

    /**
     * Gets the model produced by this handler.
     */
    public ZGaeModel getModel()
    {
        return getRootModel();
    }

    /**
     * @see org.xml.sax.ContentHandler#characters(char[], int, int)
     */
    @Override
    public void characters(char[] aCh, int aStart, int aLength) throws SAXException
    {
        String charData = new String(aCh, aStart, aLength).trim();
        if (charData.length() > 0)
            getBuffer().append(charData);
    }

    /**
     * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
     */
    @Override
    @SuppressWarnings("null")
    public void endElement(String aUri, String aLocalName, String aName) throws SAXException
    {
        ZGaeModel model = getModelStack().pop();

        if (mBuffer == null)
            return;

        String modelValueStr = getBuffer().toString();
        if (modelValueStr != null && modelValueStr.length() > 0)
        {
            Object modelValue = getTypeMapper().convertElementValue(model, modelValueStr);
            if (modelValue == null)
            {
                throwError("Error setting value on model ''{0}'' - value ''{1}'' could not be converted.",
                    model.getClass().getSimpleName(), modelValueStr);
            }
            Method mutator = getMutatorProvider().getMutatorForElementValue(model, modelValue);
            if (mutator == null)
            {
                throwError("Error setting value on model ''{0}'' - no value mutator found.",
                    model.getClass().getSimpleName());
            }
            try
            {
                mutator.invoke(model, modelValue);
            }
            catch (Exception e)
            {
                throwError("Error setting value on model ''{0}'' - failed during mutator invocation.",
                    model.getClass().getSimpleName());
            }
            setBuffer(null);
        }
    }

    /**
     * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String)
     */
    @Override
    public void endPrefixMapping(String aPrefix) throws SAXException
    {
    }

    /**
     * @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int)
     */
    @Override
    public void ignorableWhitespace(char[] aCh, int aStart, int aLength) throws SAXException
    {
    }

    /**
     * @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator)
     */
    @Override
    public void setDocumentLocator(Locator aLocator)
    {
        setLocator(aLocator);
    }

    /**
     * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
     */
    @Override
    @SuppressWarnings("null")
    public void startElement(String aUri, String aLocalName, String aName, Attributes aAtts)
    throws SAXException
    {
        QName elementName = new QName(aUri, aLocalName);

        ZGaeModel model = getModelFactory().createModel(elementName, getCurrentParent());
        if (model == null)
            throwError("Error creating model for element ''{0}''.", elementName);

        model.getNamespaceMap().putAll(getNamespaceContext());
        getNamespaceContext().clear();

        if (getRootModel() == null)
            setRootModel(model);
        else
            addNewModelToParent(model, elementName);

        setAttributeValuesOnModel(model, aAtts);
        getModelStack().push(model);

        setBuffer(null);
    }

    /**
     * Gets the current parent.
     */
    protected ZGaeModel getCurrentParent()
    {
        if (getModelStack().size() == 0)
            return null;
        return getModelStack().peek();
    }

    /**
     * @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String)
     */
    @Override
    public void startPrefixMapping(String aPrefix, String aUri) throws SAXException
    {
        getNamespaceContext().put(aPrefix, aUri);
    }

    /**
     * Adds the given model to its parent (which is pulled from the model
     * stack).
     * 
     * @param aModel
     * @param aElementName
     */
    @SuppressWarnings("null")
    protected void addNewModelToParent(ZGaeModel aModel, QName aElementName)
    {
        ZGaeModel parentModel = getCurrentParent();
        Method mutator = getMutatorForChild(aModel, aElementName, parentModel);
        if (mutator == null)
            throwError("Error adding child ''{0}'' (name: ''{1}'') to parent ''{2}'' - no mutator could be found.",
                aModel.getClass().getSimpleName(), aElementName.getLocalPart(), parentModel.getClass().getSimpleName());

        try
        {
            mutator.invoke(parentModel, aModel);
            aModel.setParent(parentModel);
        }
        catch (Exception e)
        {
            throwError("Error adding child ''{0}'' (name: ''{1}'') to parent ''{2}''.", e,
                aModel.getClass().getSimpleName(), aElementName, parentModel.getClass().getSimpleName());
        }
    }

    /**
     * Iterates through all of the attribute values and sets them on the model.
     * 
     * @param aModel
     * @param aAtts
     */
    @SuppressWarnings("null")
    protected void setAttributeValuesOnModel(ZGaeModel aModel, Attributes aAtts)
    {
        for (int idx = 0; idx < aAtts.getLength(); idx++)
        {
            String name = aAtts.getQName(idx);
            QName qname = resolveQName(name, aModel);
            String valueStr = aAtts.getValue(idx);
            Object value = convertValueToCorrectType(valueStr, aModel, qname);
            if (value == null)
            {
                throwError("Error setting attribute ''{0}'' on model ''{1}'' - the attribute value could not be converted to an appropriate type.",
                    name, aModel.getClass().getSimpleName());
            }
            Method mutator = getMutatorForAttribute(aModel, qname, value);
            if (mutator == null)
            {
                throwError("Error setting attribute ''{0}'' on model ''{1}'' - no mutator could be found for the attribute.",
                    name, aModel.getClass().getSimpleName());
            }
            try
            {
                mutator.invoke(aModel, value);
            }
            catch (Exception e)
            {
                throwError("Error setting attribute ''{0}'' on model ''{1}''.", e, name,
                    aModel.getClass().getSimpleName());
            }
        }
    }

    /**
     * Converts an attribute value to its correct type based on the parent model
     * and QName of the attribute name.
     * 
     * @param aValueStr
     * @param aModel
     * @param aQName
     */
    private Object convertValueToCorrectType(String aValueStr, ZGaeModel aModel, QName aQName)
    {
        return getTypeMapper().convertAttributeValue(aValueStr, aModel, aQName);
    }

    /**
     * Gets the mutator to use to set the given attribute value on the given model.
     * 
     * @param aModel
     * @param aQName
     * @param aValue
     */
    private Method getMutatorForAttribute(ZGaeModel aModel, QName aQName, Object aValue)
    {
        return getMutatorProvider().getMutatorForAttribute(aModel, aQName, aValue);
    }

    /**
     * Gets the mutator for adding the given child to the given parent.
     * 
     * @param aModel
     * @param aElementName
     * @param aParentModel
     */
    private Method getMutatorForChild(ZGaeModel aModel, QName aElementName, ZGaeModel aParentModel)
    {
        return getMutatorProvider().getMutatorForChild(aModel, aElementName, aParentModel);
    }

    /**
     * Throws an error with the given message and root cause.
     * 
     * @param aMessageFormat
     * @param aThrowable
     * @param aArguments
     */
    private void throwError(String aMessageFormat, Throwable aThrowable, Object ... aArguments)
    {
        throw new RuntimeException(MessageFormat.format(aMessageFormat, aArguments), aThrowable);
    }

    /**
     * Throws an error with the given message.
     * 
     * @param aMessageFormat
     * @param aArguments
     */
    private void throwError(String aMessageFormat, Object ... aArguments)
    {
        throwError(aMessageFormat, (Throwable) null, aArguments);
    }

    /**
     * Resolves the given name to a {@link QName}.
     * 
     * @param aName
     * @param aContext
     */
    protected QName resolveQName(String aEncodedQName, ZGaeModel aContext)
    {
        return aContext.resolveEncodedQName(aEncodedQName);
    }

    /**
     * @return the rootModel
     */
    protected ZGaeModel getRootModel()
    {
        return mRootModel;
    }

    /**
     * @param aRootModel the rootModel to set
     */
    protected void setRootModel(ZGaeModel aRootModel)
    {
        mRootModel = aRootModel;
    }

    /**
     * @return the modelStack
     */
    protected Stack<ZGaeModel> getModelStack()
    {
        return mModelStack;
    }

    /**
     * @param aModelStack the modelStack to set
     */
    protected void setModelStack(Stack<ZGaeModel> aModelStack)
    {
        mModelStack = aModelStack;
    }

    /**
     * @return the modelFactory
     */
    protected IZGaeModelFactory getModelFactory()
    {
        return mModelFactory;
    }

    /**
     * @param aModelFactory the modelFactory to set
     */
    protected void setModelFactory(IZGaeModelFactory aModelFactory)
    {
        mModelFactory = aModelFactory;
    }

    /**
     * @return the locator
     */
    protected Locator getLocator()
    {
        return mLocator;
    }

    /**
     * @param aLocator the locator to set
     */
    protected void setLocator(Locator aLocator)
    {
        mLocator = aLocator;
    }

    /**
     * @return the namespaceContext
     */
    protected Map<String, String> getNamespaceContext()
    {
        return mNamespaceContext;
    }

    /**
     * @param aNamespaceContext the namespaceContext to set
     */
    protected void setNamespaceContext(Map<String, String> aNamespaceContext)
    {
        mNamespaceContext = aNamespaceContext;
    }

    /**
     * @return the typeMapper
     */
    protected IZGaeModelTypeMapper getTypeMapper()
    {
        return mTypeMapper;
    }

    /**
     * @param aTypeMapper the typeMapper to set
     */
    protected void setTypeMapper(IZGaeModelTypeMapper aTypeMapper)
    {
        mTypeMapper = aTypeMapper;
    }

    /**
     * @return the mutatorProvider
     */
    protected IZGaeModelMutatorProvider getMutatorProvider()
    {
        return mMutatorProvider;
    }

    /**
     * @param aMutatorProvider the mutatorProvider to set
     */
    protected void setMutatorProvider(IZGaeModelMutatorProvider aMutatorProvider)
    {
        mMutatorProvider = aMutatorProvider;
    }

    /**
     * @return the buffer
     */
    protected StringBuffer getBuffer()
    {
        if (mBuffer == null)
            mBuffer = new StringBuffer();
        return mBuffer;
    }

    /**
     * @param aBuffer the buffer to set
     */
    protected void setBuffer(StringBuffer aBuffer)
    {
        mBuffer = aBuffer;
    }
}
